package communitycommons;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;

import com.mendix.core.Core;
import com.mendix.core.CoreException;
import com.mendix.systemwideinterfaces.core.IContext;
import com.mendix.systemwideinterfaces.core.IMendixIdentifier;
import com.mendix.systemwideinterfaces.core.IMendixObject;

public class XPath<T>
{
	/** Build in tokens, see: https://world.mendix.com/display/refguide3/XPath+Keywords+and+System+Variables
	 * 
	 */
	
	public static final String CurrentUser =  "[%CurrentUser%]";
	public static final String CurrentObject =  "[%CurrentObject%]";

	public static final String CurrentDateTime =  "[%CurrentDateTime%]";
	public static final String BeginOfCurrentDay =  "[%BeginOfCurrentDay%]";
	public static final String EndOfCurrentDay =  "[%EndOfCurrentDay%]";
	public static final String BeginOfCurrentHour =  "[%BeginOfCurrentHour%]";
	public static final String EndOfCurrentHour =  "[%EndOfCurrentHour%]";
	public static final String BeginOfCurrentMinute =  "[%BeginOfCurrentMinute%]";
	public static final String EndOfCurrentMinute =  "[%EndOfCurrentMinute%]";
	public static final String BeginOfCurrentMonth =  "[%BeginOfCurrentMonth%]";
	public static final String EndOfCurrentMonth =  "[%EndOfCurrentMonth%]";
	public static final String BeginOfCurrentWeek =  "[%BeginOfCurrentWeek%]";
	public static final String EndOfCurrentWeek =  "[%EndOfCurrentWeek%]";

	public static final String DayLength =  "[%DayLength%]";
	public static final String HourLength =  "[%HourLength%]";
	public static final String MinuteLength =  "[%MinuteLength%]";
	public static final String SecondLength =  "[%SecondLength%]";
	public static final String WeekLength =  "[%WeekLength%]";
	public static final String YearLength =  "[%YearLength%]";
	public static final String ID	= "id";
	
	/** End builtin tokens */
	
	private String	entity;
	private int	offset = 0;
	private int	limit = -1;
	private LinkedHashMap<String, String> sorting = new LinkedHashMap<String, String>(); //important, linked map!
	private LinkedList<String> closeStack = new LinkedList<String>();
	private StringBuffer builder = new StringBuffer();
	private IContext	context;
	private Class<T>	proxyClass;
	private boolean requiresBinOp = false; //state property, indicates whether and 'and' needs to be inserted before the next constraint
	
	public static XPath<IMendixObject> create(IContext c, String entityType) {
		XPath<IMendixObject> res = new XPath<IMendixObject>(c, IMendixObject.class);
		
		res.entity = entityType;
		
		return res;
	}
	
	public static <U> XPath<U> create(IContext c, Class<U> proxyClass) {
		return new XPath<U>(c, proxyClass);
	}
	
	private XPath(IContext c, Class<T> proxyClass) {
		try
		{
			if (proxyClass != IMendixObject.class)
				this.entity = (String) proxyClass.getMethod("getType").invoke(null);
		}
		catch (Exception e)
		{
			throw new IllegalArgumentException("Failed to determine entity type of proxy class. Did you provide a valid proxy class? '" + proxyClass.getName() + "'");
		}

		this.proxyClass = proxyClass;
		this.context = c;
	}
	
	private XPath<T> autoInsertAnd() {
		if (requiresBinOp)
			and();
		return this;
	}
	
	private XPath<T> requireBinOp(boolean requires) {
		requiresBinOp = requires;
		return this;
	}
	
	public XPath<T> offset(int offset2) {
		if (offset2 < 0)
			throw new IllegalArgumentException("Offset should not be negative");
		this.offset  = offset2;
		return this;
	}
	
	public XPath<T> limit(int limit2) {
		if (limit2 < -1 || limit2 == 0)
			throw new IllegalArgumentException("Limit should be larger than zero or -1. ");

		this.limit = limit2;
		return this;
	}
	
	public XPath<T> addSortingAsc(Object... sortparts) {
		assertOdd(sortparts);
		sorting.put(StringUtils.join(sortparts, '/'), "asc");
		return this;
	}
	
	public XPath<T> addSortingDesc(Object... sortparts) {
		sorting.put(StringUtils.join(sortparts, "/"), "desc");
		return this;
	}	
	
	public XPath<T> eq(Object attr, Object valuecomparison) {
		return compare(attr, "=", valuecomparison);
	}
	
	public XPath<T> eq(Object... pathAndValue) {
		assertEven(pathAndValue);
		return compare(Arrays.copyOfRange(pathAndValue, 0, pathAndValue.length -1), "=", pathAndValue[pathAndValue.length -1 ]);
	}
	
	public XPath<T> equalsIgnoreCase(Object attr, String value) {
		//(contains(Name, $email) and length(Name) = length($email)
		return 
			subconstraint()
			.contains(attr, value)
			.and()
			.append(" length(" + attr + ") = ").append(value == null ? "0" : valueToXPathValue(value.length()))	
			.close();
	}
	
	public XPath<T> notEq(Object attr, Object valuecomparison) {
		return compare(attr, "!=", valuecomparison);
	}
	
	public XPath<T> notEq(Object... pathAndValue) {
		assertEven(pathAndValue);
		return compare(Arrays.copyOfRange(pathAndValue, 0, pathAndValue.length -1), "!=", pathAndValue[pathAndValue.length -1 ]);
	}

	public XPath<T> contains(Object attr, String value)
	{
		autoInsertAnd().append(" contains(").append(String.valueOf(attr)).append(",").append(valueToXPathValue(value)).append(") ");
		return this.requireBinOp(true);
	}
	
	public XPath<T> compare(Object attr, String operator, Object value) {
		return compare(new Object[] {attr}, operator, value);
	}
	
	public XPath<T> compare(Object[] path, String operator, Object value) {
		assertOdd(path);
		autoInsertAnd().append(StringUtils.join(path, '/')).append(" ").append(operator).append(" ").append(valueToXPathValue(value));
		return this.requireBinOp(true);
	}
	
	public XPath<T> hasReference(Object... path)
	{
		assertEven(path); //Reference + entity type
		autoInsertAnd().append(StringUtils.join(path, '/'));
		return this.requireBinOp(true);
	}

	public XPath<T> subconstraint(Object... path) {
		assertEven(path);
		autoInsertAnd().append(StringUtils.join(path, '/')).append("[");
		closeStack.push("]");
		return this.requireBinOp(false);
	}	
	
	public XPath<T> subconstraint() {
		autoInsertAnd().append("(");
		closeStack.push(")");
		return this.requireBinOp(false);
	}
	
	public XPath<T> addConstraint()
	{
		if (!closeStack.isEmpty() && !closeStack.peek().equals("]"))
			throw new IllegalStateException("Cannot add a constraint while in the middle of something else..");
		
		return append("][").requireBinOp(false);
	}
	
	public XPath<T> close() {
		if (closeStack.isEmpty())
			throw new IllegalStateException("XPathbuilder close stack is empty!");
		append(closeStack.pop());
		return requireBinOp(true);
		//MWE: note that a close does not necessary require a binary operator, for example with two subsequent block([bla][boe]) constraints, 
		//but openening a binary constraint reset the flag, so that should be no issue
	}
	
	public XPath<T> or() {
		if (!requiresBinOp)
			throw new IllegalStateException("Received 'or' but no binary operator was expected");
		return append(" or ").requireBinOp(false);
	}
	
	public XPath<T> and() {
		if (!requiresBinOp)
			throw new IllegalStateException("Received 'and' but no binary operator was expected");
		return append(" and ").requireBinOp(false);
	}
	
	public XPath<T> not() {
		autoInsertAnd();
		closeStack.push(")");
		return append(" not(").requireBinOp(false);
	}
	
	private void assertOdd(Object[] stuff) {
		if (stuff == null || stuff.length == 0 || stuff.length % 2 == 0)
			throw new IllegalArgumentException("Expected an odd number of xpath path parts");
	}
	
	private void assertEven(Object[] stuff) {
		if (stuff == null || stuff.length == 0 || stuff.length % 2 == 1)
			throw new IllegalArgumentException("Expected an even number of xpath path parts");
	}
	
	public XPath<T> append(String s) {
		builder.append(s);
		return this;
	}
	
	public String getXPath() {
	
		if (builder.length() > 0)
			return "//" + this.entity + "[" + builder.toString() + "]";
		return "//" + this.entity;
	}

	public XPath<T> gt(Object attr, Object valuecomparison) {
		return compare(attr,">=", valuecomparison);
	}

	public XPath<T> gt(Object... pathAndValue) {
		assertEven(pathAndValue);
		return compare(Arrays.copyOfRange(pathAndValue, 0, pathAndValue.length -1), ">=", pathAndValue[pathAndValue.length -1 ]);
	}
	
	private void assertEmptyStack() throws IllegalStateException
	{
		if (!closeStack.isEmpty())
			throw new IllegalStateException("Invalid xpath expression, not all items where closed");
	}
	
	public long count( ) throws CoreException {
		assertEmptyStack();

		return Core.retrieveXPathQueryAggregate(context, "count(" + getXPath() +")");
	}

	
	public IMendixObject firstMendixObject() throws CoreException {
		assertEmptyStack();
		
		List<IMendixObject> result = Core.retrieveXPathQuery(context, getXPath(), 1, offset, sorting);
		if (result.isEmpty())
			return null;
		return result.get(0);
	}
	
	public T first() throws CoreException {
		return createProxy(context, proxyClass, firstMendixObject());
	}
	

	/**
	 * Given a set of attribute names and values, tries to find the first object that matches all conditions, or creates one
	 * 
	 * @param autoCommit: whether the object should be committed once created (default: true)
	 * @param keysAndValues
	 * @return
	 * @throws CoreException 
	 */
	public T findOrCreateNoCommit(Object... keysAndValues) throws CoreException
	{
		T res = findFirst(keysAndValues);
		
		return res != null ? res : constructInstance(false, keysAndValues);
	}

	
	public T findOrCreate(Object... keysAndValues) throws CoreException {
		T res = findFirst(keysAndValues);

		return res != null ? res : constructInstance(true, keysAndValues);

	}
	
	public T findOrCreateSynchronized(Object... keysAndValues) throws CoreException, InterruptedException {
		T res = findFirst(keysAndValues);
		
		if (res != null) {
			return res;
		} else {
			synchronized (Core.getMetaObject(entity)) {
				IContext synchronizedContext = context.getSession().createContext().createSudoClone();
				try {
					synchronizedContext.startTransaction();
					res = createProxy(synchronizedContext, proxyClass, XPath.create(synchronizedContext, entity).findOrCreate(keysAndValues));
					synchronizedContext.endTransaction();
					return res;
				} catch (CoreException e) {
					if (synchronizedContext.isInTransaction()) {
						synchronizedContext.rollbackTransAction();
					}
					throw e;
				}
			}
		}
	}	
	
	public T findFirst(Object... keysAndValues)
			throws IllegalStateException, CoreException
	{
		if (builder.length() > 0)
			throw new IllegalStateException("FindFirst can only be used on XPath which do not have constraints already");
		
		assertEven(keysAndValues);
		for(int i = 0; i < keysAndValues.length; i+= 2)
			eq(keysAndValues[i], keysAndValues[i + 1]);
		
		T res = this.first();
		return res;
	}
	

	/**
	 * Creates one instance of the type of this XPath query, and initializes the provided attributes to the provided values. 
	 * @param keysAndValues AttributeName, AttributeValue, AttributeName2, AttributeValue2... list. 
	 * @return
	 * @throws CoreException
	 */
	public T constructInstance(boolean autoCommit, Object... keysAndValues) throws CoreException
	{
		assertEven(keysAndValues);
		IMendixObject newObj = Core.instantiate(context, this.entity);
		
		for(int i = 0; i < keysAndValues.length; i+= 2)
			newObj.setValue(context, String.valueOf(keysAndValues[i]), toMemberValue(keysAndValues[i + 1]));
		
		if (autoCommit)
			Core.commit(context, newObj);
		
		return createProxy(context, proxyClass, newObj);
	}
	
	/**
	 * Given a current collection of primitive values, checks if for each value in the collection an object in the database exists. 
	 * It creates a new object if needed, and removes any superfluos objects in the database that are no longer in the collection.  
	 * 
	 * @param currentCollection The collection that act as reference for the objects that should be in this database in the end. 
	 * @param comparisonAttribute The attribute that should store the value as decribed in the collection
	 * @param autoDelete Automatically remove any superfluous objects form the database
	 * @param keysAndValues Constraints that should hold for the set of objects that are deleted or created. Objects outside this constraint are not processed.
	 * 
	 * @return A pair of lists. The first list contains the newly created objects, the second list contains the objects that (should be or are) removed. 
	 * @throws CoreException
	 */
	public <U> ImmutablePair<List<T>, List<T>> syncDatabaseWithCollection(Collection<U> currentCollection, Object comparisonAttribute, boolean autoDelete, Object... keysAndValues) throws CoreException {
		if (builder.length() > 0)
			throw new IllegalStateException("syncDatabaseWithCollection can only be used on XPath which do not have constraints already");

		
		List<T> added = new ArrayList<T>();
		List<T> removed = new ArrayList<T>();
		
		Set<U> col = new HashSet<U>(currentCollection);
		
		for(int i = 0; i < keysAndValues.length; i+= 2)
			eq(keysAndValues[i], keysAndValues[i + 1]);
		
		for(IMendixObject existingItem : this.allMendixObjects()) {
			//Item is still available 
			if (col.remove(existingItem.getValue(context, String.valueOf(comparisonAttribute)))) 
				continue;
				
			//No longer available
			removed.add(createProxy(context, this.proxyClass, existingItem));
			if (autoDelete) 
				Core.delete(context, existingItem);
		}
		
		//Some items where not found in the database
		for(U value : col) {
			
			//In apache lang3, this would just be: ArrayUtils.addAll(keysAndValues, comparisonAttribute, value)
			Object[] args = new Object[keysAndValues.length + 2];
			for(int i = 0; i < keysAndValues.length; i++)
				args[i] = keysAndValues[i];
			args[keysAndValues.length] = comparisonAttribute;
			args[keysAndValues.length + 1] = value;
			
			T newItem = constructInstance(true, args);
			added.add(newItem);
		}
		
		//Oké, stupid, Pair is also only available in apache lang3, so lets use a simple pair implementation for now
		return ImmutablePair.of(added, removed);
	}
	
	public T firstOrWait(long timeoutMSecs) throws CoreException, InterruptedException
	{
		IMendixObject result = null;
		
		long start = System.currentTimeMillis();
		int  sleepamount = 200;
		int  loopcount = 0;
		
		while (result == null) {
			loopcount += 1;
			result = firstMendixObject();
			
			long now = System.currentTimeMillis();
			
			if (start + timeoutMSecs < now) //Time expired
				break;

			if (loopcount % 5 == 0)
				sleepamount *= 1.5;
			
			//not expired, wait a bit
			if (result == null)
				Thread.sleep(sleepamount);
		}
		
		return createProxy(context, proxyClass, result); 
	}
	
	
	public List<IMendixObject> allMendixObjects() throws CoreException {
		assertEmptyStack();

		return Core.retrieveXPathQuery(context, getXPath(), limit, offset, sorting);
	}
	
	public List<T> all() throws CoreException {
		List<T> res = new ArrayList<T>();
		for(IMendixObject o : allMendixObjects())
			res.add(createProxy(context, proxyClass, o));
		
		return res;
	}
	
	@Override
	public String toString() {
		return getXPath();
	}


	/** 
	 * 
	 * 
	 * Static utility functions 
	 * 
	 * 
	*/
	
	//cache for proxy constructors. Reflection is slow, so reuse as much as possible
	private static Map<String, Method> initializers = new HashMap<String, Method>();
	
	public static <T> List<T> createProxyList(IContext c, Class<T> proxieClass, List<IMendixObject> objects) {
		List<T> res = new ArrayList<T>();
		if (objects == null || objects.size() == 0)
			return res;
		
		for(IMendixObject o : objects)
			res.add(createProxy(c, proxieClass, o));
		
		return res;
	}
	
	public static <T> T createProxy(IContext c, Class<T> proxieClass, IMendixObject object) {
		//Borrowed from nl.mweststrate.pages.MxQ package
		
		if (object == null)
			return null;
		
		if (c == null || proxieClass == null)
			throw new IllegalArgumentException("[CreateProxy] No context or proxieClass provided. ");
		
		//jeuj, we expect IMendixObject's. Thats nice..
		if (proxieClass == IMendixObject.class)
			return proxieClass.cast(object); //.. since we can do a direct cast
		
		try {
			String entityType = object.getType();
			
			if (!initializers.containsKey(entityType)) { 
			
				String[] entType = object.getType().split("\\.");
				Class<?> realClass = Class.forName(entType[0].toLowerCase()+".proxies."+entType[1]);

				initializers.put(entityType, realClass.getMethod("initialize", IContext.class, IMendixObject.class));
			}
			
			//find constructor
			Method m = initializers.get(entityType);
			
			//create proxy object
			Object result = m.invoke(null, c, object);
			
			//cast, but check first is needed because the actual type might be a subclass of the requested type
			if (!proxieClass.isAssignableFrom(result.getClass()))
				throw new IllegalArgumentException("The type of the object ('" + object.getType() + "') is not (a subclass) of '" + proxieClass.getName()+"'");
			
			T proxie = proxieClass.cast(result);
			return proxie;
		}
		catch (Exception e) {
			throw new RuntimeException("Unable to instantiate proxie: " + e.getMessage(), e);
		}
	}
	
	public static String valueToXPathValue(Object value)
	{
		if (value == null)
			return "NULL"; 

		//Complex objects
		if (value instanceof IMendixIdentifier)
			return "'" + String.valueOf(((IMendixIdentifier) value).toLong()) +  "'";
		if (value instanceof IMendixObject)
			return valueToXPathValue(((IMendixObject)value).getId());
		if (value instanceof List<?>)
			throw new IllegalArgumentException("List based values are not supported!");
		
		//Primitives
		if (value instanceof Date)
			return String.valueOf(((Date) value).getTime());
		if (value instanceof Long || value instanceof Integer)
			return String.valueOf(value);
		if (value instanceof Double || value instanceof Float) {
			//make sure xpath understands our number formatting
			NumberFormat format = NumberFormat.getNumberInstance(Locale.ENGLISH);
			format.setMaximumFractionDigits(10);
			format.setGroupingUsed(false);
			return format.format(value);
		}
		if (value instanceof Boolean) {
			return value.toString() + "()"; //xpath boolean, you know..
		}
		if (value instanceof String) {
			return "'" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "'";
		}
		
		//Object, assume its a proxy and deproxiefy
		try
		{
			IMendixObject mo = proxyToMendixObject(value);
			return valueToXPathValue(mo);
		}
		catch (NoSuchMethodException e)
		{
			//This is O.K. just not a proxy object...
		}
		catch (Exception e) {
			throw new RuntimeException("Failed to retrieve MendixObject from proxy: " + e.getMessage(), e);
		}
		
		//assume some string representation
		return "'" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "'";
	}
	
	public static IMendixObject proxyToMendixObject(Object value)
		throws NoSuchMethodException, SecurityException, IllegalAccessException,
		IllegalArgumentException, InvocationTargetException
	{
		Method m = value.getClass().getMethod("getMendixObject");
		IMendixObject mo = (IMendixObject) m.invoke(value);
		return mo;
	}

	public static <T> List<IMendixObject> proxyListToMendixObjectList(
			List<T> objects) throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
	{
		ArrayList<IMendixObject> res = new ArrayList<IMendixObject>(objects.size());
		for(T i : objects)
			res.add(proxyToMendixObject(i));
		return res;
	}
	
	public static Object toMemberValue(Object value)
	{
		if (value == null)
			return null; 

		//Complex objects
		if (value instanceof IMendixIdentifier)
			return value;
		if (value instanceof IMendixObject)
			return ((IMendixObject)value).getId();
		
		if (value instanceof List<?>)
			throw new IllegalArgumentException("List based values are not supported!");
		
		//Primitives
		if (   value instanceof Date 
				|| value instanceof Long 
				|| value instanceof Integer
			  || value instanceof Double 
			  || value instanceof Float
			  || value instanceof Boolean
			  || value instanceof String) {
			return value;
		}
		
		if (value.getClass().isEnum())
			return value.toString();

		//Object, assume its a proxy and deproxiefy
		try
		{
			Method m = value.getClass().getMethod("getMendixObject");
			IMendixObject mo = (IMendixObject) m.invoke(value);
			return toMemberValue(mo);
		}
		catch (NoSuchMethodException e)
		{
			//This is O.K. just not a proxy object...
		}
		catch (Exception e) {
			throw new RuntimeException("Failed to convert object to IMendixMember compatible value '" + value + "': " + e.getMessage(), e);
		}
		
		throw new RuntimeException("Failed to convert object to IMendixMember compatible value: " + value);
	}
	
	public static interface IBatchProcessor<T> {
		public void onItem(T item, long offset, long total) throws Exception;
	}
	
	private static final class ParallelJobRunner<T> implements Callable<Boolean>
	{
		private final XPath<T> self;
		private final IBatchProcessor<T> batchProcessor;
		private final IMendixObject item;
		private long index;
		private long count;

		ParallelJobRunner(XPath<T> self, IBatchProcessor<T> batchProcessor, IMendixObject item, long index, long count)
		{
			this.self = self;
			this.batchProcessor = batchProcessor;
			this.item = item;
			this.index = index;
			this.count = count;
		}

		@Override
		public Boolean call()
		{
			try
			{
				batchProcessor.onItem(XPath.createProxy(Core.createSystemContext(), self.proxyClass, item), index, count); //mwe: hmm, many contexts..
				return true;
			}
			catch (Exception e)
			{
				throw new RuntimeException(String.format("Failed to execute batch on '%s' offset %d: %s", self.toString(), self.offset, e.getMessage()), e);
			}
		}
	}
	

	/**
	 * Retreives all items in this xpath query in batches of a limited size. 
	 * Not that this function does not start a new transaction for all the batches,
	 * rather, it just limits the number of objects being retrieved and kept in memory at the same time. 
	 * 
	 * So it only batches the retrieve process, not the optional manipulations done in the onItem method.  
	 * @param batchsize
	 * @param batchProcessor
	 * @throws CoreException
	 */
	public void batch(int batchsize, IBatchProcessor<T> batchProcessor) throws CoreException
	{
		if (this.sorting.isEmpty())
			this.addSortingAsc(XPath.ID);
		
		long count = this.count();
		
		int baseoffset = this.offset;
		int baselimit = this.limit;
		
		boolean useBaseLimit = baselimit > -1;
		
		this.offset(baseoffset);
		List<T> data;
		
		long i = 0;

		do {
			int newlimit = useBaseLimit ? Math.min(batchsize, baseoffset + baselimit - this.offset) : batchsize;
			if (newlimit == 0)
				break; //where done, no more data is needed
			
			this.limit(newlimit);
			data = this.all();

			for(T item : data) {
				i += 1;
				try
				{
					batchProcessor.onItem(item, i, Math.max(i, count));
				}
				catch (Exception e)
				{
					throw new RuntimeException(String.format("Failed to execute batch on '%s' offset %d: %s", this.toString(), this.offset, e.getMessage()), e);
				}
			}
			
			this.offset(this.offset + data.size());
		} while(data.size() > 0);
	}
	
	/**
	 * Batch with parallelization.
	 * 
	 * IMPORTANT NOTE: DO NOT USE THE CONTEXT OF THE XPATH OBJECT ITSELF INSIDE THE BATCH PROCESSOR!
	 * 
	 * Instead, use: Item.getContext(); !!
	 * 
	 * 
	 * @param batchsize
	 * @param threads
	 * @param batchProcessor
	 * @throws CoreException
	 * @throws InterruptedException
	 * @throws ExecutionException
	 */
	public void batch(int batchsize, int threads, final IBatchProcessor<T> batchProcessor)
		throws CoreException, InterruptedException, ExecutionException
	{
		if (this.sorting.isEmpty())
			this.addSortingAsc(XPath.ID);

		ExecutorService pool = Executors.newFixedThreadPool(threads);

		final long count = this.count();

		final XPath<T> self = this;

		int progress = 0;
		List<Future<?>> futures = new ArrayList<Future<?>>(batchsize); //no need to synchronize

		this.offset(0);
		this.limit(batchsize);

		List<IMendixObject> data = this.allMendixObjects();

		while (data.size() > 0)
		{

			for (final IMendixObject item : data)
			{
				futures.add(pool.submit(new ParallelJobRunner<T>(self, batchProcessor, item, progress, count)));
				progress += 1;
			}

			while (!futures.isEmpty())
				futures.remove(0).get(); //wait for all futures before proceeding to next iteration

			this.offset(this.offset + data.size());
			data = this.allMendixObjects();
		}

		if (pool.shutdownNow().size() > 0)
			throw new IllegalStateException("Not all tasks where finished!");

	}

	public static Class<?> getProxyClassForEntityName(String entityname)
	{
		{
			String [] parts = entityname.split("\\.");
			try
			{
				return Class.forName(parts[0].toLowerCase() + ".proxies." +  parts[1]);
			}
			catch (ClassNotFoundException e)
			{
				throw new RuntimeException("Cannot find class for entity: " + entityname + ": " + e.getMessage(), e);
			}
		}
	}

	public boolean deleteAll() throws CoreException
	{
		this.limit(1000);
		List<IMendixObject> objs = allMendixObjects();
		while (!objs.isEmpty()) {
			if (!Core.delete(context, objs.toArray(new IMendixObject[objs.size()]))) 
				return false; //TODO: throw?
			
			objs = allMendixObjects();
		}
		return true;
	}



}
